package flexo.graphic;

import java.io.DataInputStream;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;

import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.*;

import flexo.common.LogMsgType;

/**
 * The shader must be composed of a fragment shader (.fs extension) and a vertex shader
 * (.vs extension). The first lines of the vertex shader must have all the uniform 
 * variables or '//' commentaries. * If you follow this, the variables' name will be parsed 
 * and added automatically to the engine.
 * 
 * While loading a shader, the VS and FS must have the same name, only the extension will be 
 * different. The 'name' of the shader that is required by the engine will be the path of the
 * file without the extension. For example, if you saved a default.vs and default.fs in the 
 * directory "/media/shaders/", the name of the shader will be "/media/shaders/default". 
 * 
 * @author Miguel
 *
 */
public class Shader extends flexo.common.ReferenceCount
{
	protected int texOrderCounter = 0;
	protected String shaderName;
	
	class ShaderParamInfo
	{
		int id;
		String name;
		String type;
		int texOrder = -1;
	}
	
	protected int vertexShader;
	protected int fragmentShader;
	
	protected int shader;
	
	ArrayList<ShaderParamInfo> params;	
	
	public Shader(String vertexShaderFile, String fragmentShaderFile)
	{
		create(vertexShaderFile,fragmentShaderFile);
	}
	
	public boolean isParam(String name)
	{
		for (ShaderParamInfo p : params)
		{
			if (p.name.equals(name))
			{
				return true;
			}
		}
		return false;
	}
	
	public String getName()
	{
		return shaderName;
	}
	
	protected void create(String vertexShaderFile, String fragmentShaderFile)
	{
		shaderName = vertexShaderFile.substring(0, vertexShaderFile.length()-3);
		
		flexo.Kernel.log("flexo.graphic.Shader", "Creating shader '"+shaderName+"'");
		
		vertexShader = ARBShaderObjects.glCreateShaderObjectARB(ARBVertexShader.GL_VERTEX_SHADER_ARB);
		fragmentShader = ARBShaderObjects.glCreateShaderObjectARB(ARBFragmentShader.GL_FRAGMENT_SHADER_ARB);
		
		ByteBuffer shaderSource = getProgramCode(vertexShaderFile);		
		ARBShaderObjects.glShaderSourceARB(vertexShader, shaderSource);
		ARBShaderObjects.glCompileShaderARB(vertexShader);
		
		shaderSource = getProgramCode(fragmentShaderFile);		
		ARBShaderObjects.glShaderSourceARB(fragmentShader, shaderSource);
		ARBShaderObjects.glCompileShaderARB(fragmentShader);
		
		shader  = ARBShaderObjects.glCreateProgramObjectARB();
		ARBShaderObjects.glAttachObjectARB(shader, vertexShader);
		ARBShaderObjects.glAttachObjectARB(shader, fragmentShader);
		
		ARBShaderObjects.glLinkProgramARB(shader);
		
		ARBShaderObjects.glValidateProgramARB(shader);
		if (GL11.glGetError() == GL11.GL_INVALID_OPERATION)
		{
			flexo.Kernel.log("flexo.graphic.Shader", "Shader '" + shaderName + "' doesn't compile", LogMsgType.ERROR);
			flexo.Kernel.error();
		}
		
		params = new ArrayList<ShaderParamInfo>();
		fillParamNames(vertexShaderFile);
		fillParamNames(fragmentShaderFile);
				
		if (params.size() > 0)
		{
			for (ShaderParamInfo p : params)
			{
				String s = p.name;
				ByteBuffer paramName = toByteString(s, true);
				int location = ARBShaderObjects.glGetUniformLocationARB(shader, paramName);
				p.id = location;
			}
		}
	}
	
	protected ShaderParamInfo get(String name)
	{
		for (ShaderParamInfo p : params)
		{
			if (p.name.equals(name))
			{
				return p;
			}
		}
		return null;
	}
	
	public void set(String name, int value)
	{
		ARBShaderObjects.glUniform1iARB(get(name).id, value);
	}
	
	public void set(String name, float value)
	{
		ARBShaderObjects.glUniform1fARB(get(name).id, value);
	}
	
	public void set(String name, flexo.math.Vector value)
	{
		ARBShaderObjects.glUniform3fARB(get(name).id, value.x, value.y, value.z);
	}
	
	public void set(String name, flexo.math.Matrix value, boolean transposed)
	{
		ARBShaderObjects.glUniformMatrix4ARB(get(name).id, transposed, value.getDataBuffer());
	}
	
	public void set(String name, Color value)
	{
		ARBShaderObjects.glUniform4fARB(get(name).id, value.red, value.green, value.blue, value.alpha);
	}
	
	public void set(String name, Texture tex)
	{
		ShaderParamInfo spi = get(name);
		switch(spi.texOrder)
		{
		case 0:
			ARBMultitexture.glActiveTextureARB( ARBMultitexture.GL_TEXTURE0_ARB );break;
		case 1:
			ARBMultitexture.glActiveTextureARB( ARBMultitexture.GL_TEXTURE1_ARB );break;
		case 2:
			ARBMultitexture.glActiveTextureARB( ARBMultitexture.GL_TEXTURE2_ARB );break;
		case 3:
			ARBMultitexture.glActiveTextureARB( ARBMultitexture.GL_TEXTURE3_ARB );break;
		case 4:
			ARBMultitexture.glActiveTextureARB( ARBMultitexture.GL_TEXTURE4_ARB );break;
		case 5:
			ARBMultitexture.glActiveTextureARB( ARBMultitexture.GL_TEXTURE5_ARB );break;
		case 6:
			ARBMultitexture.glActiveTextureARB( ARBMultitexture.GL_TEXTURE6_ARB );break;
		case 7:
			ARBMultitexture.glActiveTextureARB( ARBMultitexture.GL_TEXTURE7_ARB );break;
		}
		tex.bind();
		ARBShaderObjects.glUniform1iARB(spi.id, spi.texOrder);
	}
	
	public void destroy()
	{
		ARBShaderObjects.glDetachObjectARB(shader,vertexShader);
		ARBShaderObjects.glDetachObjectARB(shader,fragmentShader);
		ARBShaderObjects.glDeleteObjectARB(vertexShader);
		ARBShaderObjects.glDeleteObjectARB(fragmentShader);
		ARBShaderObjects.glDeleteObjectARB(shader);
		flexo.graphic.GraphicSystem gs = (flexo.graphic.GraphicSystem)flexo.Kernel.getSystem(flexo.graphic.GraphicSystem.ID);
		gs.shaderPool.remove(this);
	}
	
	private void fillParamNames(String fileName)
	{
		flexo.file.FileSystem fs = (flexo.file.FileSystem)flexo.Kernel.getSystem(flexo.file.FileSystem.ID);
		flexo.file.File file = fs.createFile(fileName);
		file.load(new flexo.file.reader.ShaderParam());
		for (int i = 0; i < file.getRootNode().getPropertyCount(); i++)
		{
			ShaderParamInfo p = new ShaderParamInfo();
			p.name = file.getRootNode().getProperty(i).name;
			
			// We add the parameter only if is new
			// (the fs and the vs could access to the same parameter)
			if (get(p.name) == null)
			{			
				p.type = file.getRootNode().getProperty(i).getStringValue();
				if (p.type.length() > 7 && p.type.substring(0,7).equals("sampler"))
				{
					p.texOrder = texOrderCounter++;
				}
				params.add(p);
			}
		}
	}
	
	private ByteBuffer toByteString(String str, boolean isNullTerminated)
	{
		int length = str.length();
		if (isNullTerminated)
			length++;
		ByteBuffer buff = BufferUtils.createByteBuffer(length);
		buff.put( str.getBytes() );
 
		if (isNullTerminated)
			buff.put( (byte)0 );
 
		buff.flip();
		return buff;
	}
	
	public void bind()
	{
		ARBShaderObjects.glUseProgramObjectARB(shader);
	}	
	
	private static ByteBuffer getProgramCode(String filename)
	{
		flexo.file.FileSystem fs = (flexo.file.FileSystem)flexo.Kernel.getSystem(flexo.file.FileSystem.ID);
		InputStream fileInputStream = fs.getStream(filename);
		byte[] shaderCode = null;
 
		try
		{			
			DataInputStream dataStream = new DataInputStream(fileInputStream);
			dataStream.readFully(shaderCode = new byte[ fileInputStream.available() ]);
			fileInputStream.close();
			dataStream.close();
		}
		catch (Exception e)
		{
			flexo.Kernel.log("flexo.graphic.Shader", e.getMessage(), LogMsgType.ERROR);
			flexo.Kernel.error();
		}
		ByteBuffer shaderPro = BufferUtils.createByteBuffer(shaderCode.length);
 
		shaderPro.put(shaderCode);
		shaderPro.flip();
 
		return shaderPro;
	}
}
